package test;

import java.io.EOFException;
import java.io.IOException;
import java.net.ConnectException;
import java.net.InetAddress;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;

import no.ntnu.fp.net.cl.ClException;
import no.ntnu.fp.net.cl.KtnDatagram;
import no.ntnu.fp.net.cl.KtnDatagram.Flag;
import no.ntnu.fp.net.co.Connection;

public class ConnectionPro extends no.ntnu.fp.net.co.AbstractConnection {
	
	static final int MAX_TRIES = 4;
	
	public ConnectionPro(int myPort){
		
		super();
		this.myPort = myPort;
		this.myAddress = getIPv4Address();
	}

	@Override
	public void connect(InetAddress remoteAddress, int remotePort)
			throws IOException, SocketTimeoutException {
		
		if (state != State.CLOSED) {
			System.out.println("Client can not initiate connection while not closed");
			return;
		}
		
		this.remoteAddress = remoteAddress.getHostAddress();
		this.remotePort = remotePort;
		
		KtnDatagram syn = constructInternalPacket(Flag.SYN);
		KtnDatagram synack = null;
		state = State.SYN_SENT;
		do {
			try {
				simplySendPacket(syn);
				synack = receiveAck();
			} catch (ClException e) { }
		} while (!isValid(synack));
		
		if (isValid(synack)) {
			// TODO: Implement resend
			this.remotePort = synack.getSrc_port();
			sendAck(synack, false);
			state = State.ESTABLISHED;
		} 
		else {
			System.out.println("CLIENT: Conncetion failed");	
		}
	}

	@Override
	public Connection accept() throws IOException, SocketTimeoutException {
		
		if (state != State.CLOSED) {
			System.out.println("Server can not accept connection while not closed");
			return null;
		}
		
		state = State.LISTEN;
		int tries = 0;
		
		KtnDatagram syn = null;
		do {
			syn = receivePacket(true);
		} while (!isValid(syn));
		
		state = State.CLOSED;
		
		ConnectionPro connection = new ConnectionPro(myPort + 5);
		
		connection.remoteAddress = syn.getSrc_addr();
		connection.remotePort = syn.getSrc_port();
		connection.state = State.SYN_RCVD;
		
		KtnDatagram finalAck = null;
		do {
			connection.sendAck(syn, true);
			finalAck = connection.receiveAck();
		} while (!connection.isValid(finalAck) && tries++ <= MAX_TRIES);
		
		if (connection.isValid(finalAck)) {
			System.out.println("SERVER: Connection established");
			connection.state = State.ESTABLISHED;
			return connection;
		}
		
		return null;
	}

	@Override
	public void send(String msg) throws ConnectException, IOException {
		
		KtnDatagram dataPacket = constructDataPacket(msg);
		
		try {
			simplySendPacket(dataPacket);
		} catch (ClException e) {}
		
	}

	@Override
	public String receive() throws ConnectException, IOException {
		
		if (state != State.ESTABLISHED) {
			System.out.println("Can not receive data while connection is not established");
			return null;
		}
		
		KtnDatagram dataPacket = null;
		
		try {
			dataPacket = receivePacket(false);
		} catch (EOFException e) {
			System.out.println("SERVER: FIN received.. closing..");
			close();
		}
		
		System.out.println(dataPacket.getPayload());
		
		return null;
	}

	@Override
	public void close() throws IOException {
		
		if (state != State.ESTABLISHED) {
			System.out.println("Can not close a connection when not established");
			return;
		}
		
		// Client side
		if (disconnectRequest == null) {
			
			KtnDatagram fin = constructInternalPacket(Flag.FIN);
			KtnDatagram ack = null;
			int tries = 0;
			do {
				try {
					state = State.ESTABLISHED;
					simplySendPacket(fin);
					
					state = State.FIN_WAIT_1;
					ack = receiveAck();
				} catch (ClException e) {}
			} while (!isValid(ack) /*&& (tries++ < MAX_TRIES)*/);
			
			if (!isValid(ack)) {
				System.out.println("CLIENT: ACK not received");
				return;
			}
			
			state = State.FIN_WAIT_2;
			
			KtnDatagram finalFin = null;
			System.out.println("CLIENT: WAITING FOR FIN!!!");
			do {
				
				finalFin = receivePacket(true);
				
			} while (!isValid(finalFin));
			
			sendAck(finalFin, false);
			state = State.CLOSED;
			System.out.println("CLIENT: Connection closed");
		}
		
		// Server side
		else {
			
			sendAck(disconnectRequest, false);
			state = State.CLOSE_WAIT;
			
			try {
				Thread.sleep(2000);
			} catch (InterruptedException e) {}
			
			KtnDatagram fin = constructInternalPacket(Flag.FIN);
			KtnDatagram ack = null;
			
			do {
				try {
					state = State.CLOSE_WAIT;
					simplySendPacket(fin);
					
					state = State.LAST_ACK;
					ack = receiveAck();
				} catch (ClException e) {}
			} while (!isValid(ack));
			
			if (isValid(ack)) {
				state = State.CLOSED;
				System.out.println("SERVER: Connection closed");
			}
		}
		
	}

	@Override
	protected boolean isValid(KtnDatagram packet) {
		
		if (packet == null) {
			return false;
		}
		else if (packet.getChecksum() != packet.calculateChecksum()) {
			return false;
		}
		
		return isValidByState(packet);
	}
	
	private boolean isValidByState(KtnDatagram packet) {
		
		Flag f = packet.getFlag();
		
		switch (state) {
		
		case LISTEN:		return f == Flag.SYN;
		case SYN_RCVD:		return f == Flag.ACK;
		case SYN_SENT:		return f == Flag.SYN_ACK;
		case ESTABLISHED:	return f == Flag.FIN || f == Flag.NONE || f == Flag.ACK;
		case LAST_ACK:		return f == Flag.ACK;
		case FIN_WAIT_1:	return f == Flag.ACK;
		case FIN_WAIT_2:	return f == Flag.FIN;
		
		default: return false;
		}
		
		
	}
	
	private String getIPv4Address() {
        try {
            return InetAddress.getLocalHost().getHostAddress();
        }
        catch (UnknownHostException e) {
        	System.out.println("Host set to default!");
            return "127.0.0.1";
        }
	}

}
